/*
 * Copyright 2010 Mttang.com All right reserved. This software is the
 * confidential and proprietary information of Mttang.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with Mttang.com.
 */
package com.mttang.util;

import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * 
 * @author denghongyu
 * 
 */
public abstract class CharacterEncoder {

	/** Stream that understands "printing" */
	protected PrintStream pStream;

	/** Return the number of bytes per atom of encoding */
	abstract protected int bytesPerAtom();

	/** Return the number of bytes that can be encoded per line */
	abstract protected int bytesPerLine();

	/**
	 * Encode the prefix for the entire buffer. By default is simply opens the
	 * PrintStream for use by the other functions.
	 */
	protected void encodeBufferPrefix(OutputStream aStream) throws IOException {
		pStream = new PrintStream(aStream);
	}

	/**
	 * Encode the suffix for the entire buffer.
	 */
	protected void encodeBufferSuffix(OutputStream aStream) throws IOException {
	}

	/**
	 * Encode the prefix that starts every output line.
	 */
	protected void encodeLinePrefix(OutputStream aStream, int aLength) throws IOException {
	}

	/**
	 * Encode the suffix that ends every output line. By default this method
	 * just prints a <newline> into the output stream.
	 */
	protected void encodeLineSuffix(OutputStream aStream) throws IOException {
		pStream.println();
	}

	/** Encode one "atom" of information into characters. */
	abstract protected void encodeAtom(OutputStream aStream, byte someBytes[], int anOffset, int aLength)
			throws IOException;

	/**
	 * This method works around the bizarre semantics of BufferedInputStream's
	 * read method.
	 */
	protected int readFully(InputStream in, byte buffer[]) throws java.io.IOException {
		for (int i = 0; i < buffer.length; i++) {
			int q = in.read();
			if (q == -1)
				return i;
			buffer[i] = (byte) q;
		}
		return buffer.length;
	}

	/**
	 * Encode bytes from the input stream, and write them as text characters to
	 * the output stream. This method will run until it exhausts the input
	 * stream, but does not print the line suffix for a final line that is
	 * shorter than bytesPerLine().
	 */
	public void encode(InputStream inStream, OutputStream outStream) throws IOException {
		int j;
		int numBytes;
		byte tmpbuffer[] = new byte[bytesPerLine()];

		encodeBufferPrefix(outStream);

		while (true) {
			numBytes = readFully(inStream, tmpbuffer);
			if (numBytes == 0) {
				break;
			}
			encodeLinePrefix(outStream, numBytes);
			for (j = 0; j < numBytes; j += bytesPerAtom()) {

				if ((j + bytesPerAtom()) <= numBytes) {
					encodeAtom(outStream, tmpbuffer, j, bytesPerAtom());
				} else {
					encodeAtom(outStream, tmpbuffer, j, (numBytes) - j);
				}
			}
			if (numBytes < bytesPerLine()) {
				break;
			} else {
				encodeLineSuffix(outStream);
			}
		}
		encodeBufferSuffix(outStream);
	}

	/**
	 * Encode the buffer in <i>aBuffer</i> and write the encoded result to the
	 * OutputStream <i>aStream</i>.
	 */
	public void encode(byte aBuffer[], OutputStream aStream) throws IOException {
		ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
		encode(inStream, aStream);
	}

	/**
	 * A 'streamless' version of encode that simply takes a buffer of bytes and
	 * returns a string containing the encoded buffer.
	 */
	public String encode(byte aBuffer[]) {
		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
		String retVal = null;
		try {
			encode(inStream, outStream);
			// explicit ascii->unicode conversion
			retVal = outStream.toString("8859_1");
		} catch (Exception IOException) {
			// This should never happen.
			throw new Error("CharacterEncoder.encode internal error");
		}
		return (retVal);
	}

	/**
	 * Return a byte array from the remaining bytes in this ByteBuffer.
	 * <P>
	 * The ByteBuffer's position will be advanced to ByteBuffer's limit.
	 * <P>
	 * To avoid an extra copy, the implementation will attempt to return the
	 * byte array backing the ByteBuffer. If this is not possible, a new byte
	 * array will be created.
	 */
	private byte[] getBytes(ByteBuffer bb) {
		/*
		 * This should never return a BufferOverflowException, as we're careful
		 * to allocate just the right amount.
		 */
		byte[] buf = null;

		/*
		 * If it has a usable backing byte buffer, use it. Use only if the array
		 * exactly represents the current ByteBuffer.
		 */
		if (bb.hasArray()) {
			byte[] tmp = bb.array();
			if ((tmp.length == bb.capacity()) && (tmp.length == bb.remaining())) {
				buf = tmp;
				bb.position(bb.limit());
			}
		}

		if (buf == null) {
			/*
			 * This class doesn't have a concept of encode(buf, len, off), so if
			 * we have a partial buffer, we must reallocate space.
			 */
			buf = new byte[bb.remaining()];

			/*
			 * position() automatically updated
			 */
			bb.get(buf);
		}

		return buf;
	}

	/**
	 * Encode the <i>aBuffer</i> ByteBuffer and write the encoded result to the
	 * OutputStream <i>aStream</i>.
	 * <P>
	 * The ByteBuffer's position will be advanced to ByteBuffer's limit.
	 */
	public void encode(ByteBuffer aBuffer, OutputStream aStream) throws IOException {
		byte[] buf = getBytes(aBuffer);
		encode(buf, aStream);
	}

	/**
	 * A 'streamless' version of encode that simply takes a ByteBuffer and
	 * returns a string containing the encoded buffer.
	 * <P>
	 * The ByteBuffer's position will be advanced to ByteBuffer's limit.
	 */
	public String encode(ByteBuffer aBuffer) {
		byte[] buf = getBytes(aBuffer);
		return encode(buf);
	}

	/**
	 * Encode bytes from the input stream, and write them as text characters to
	 * the output stream. This method will run until it exhausts the input
	 * stream. It differs from encode in that it will add the line at the end of
	 * a final line that is shorter than bytesPerLine().
	 */
	public void encodeBuffer(InputStream inStream, OutputStream outStream) throws IOException {
		int j;
		int numBytes;
		byte tmpbuffer[] = new byte[bytesPerLine()];

		encodeBufferPrefix(outStream);

		while (true) {
			numBytes = readFully(inStream, tmpbuffer);
			if (numBytes == 0) {
				break;
			}
			encodeLinePrefix(outStream, numBytes);
			for (j = 0; j < numBytes; j += bytesPerAtom()) {
				if ((j + bytesPerAtom()) <= numBytes) {
					encodeAtom(outStream, tmpbuffer, j, bytesPerAtom());
				} else {
					encodeAtom(outStream, tmpbuffer, j, (numBytes) - j);
				}
			}
			encodeLineSuffix(outStream);
			if (numBytes < bytesPerLine()) {
				break;
			}
		}
		encodeBufferSuffix(outStream);
	}

	/**
	 * Encode the buffer in <i>aBuffer</i> and write the encoded result to the
	 * OutputStream <i>aStream</i>.
	 */
	public void encodeBuffer(byte aBuffer[], OutputStream aStream) throws IOException {
		ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
		encodeBuffer(inStream, aStream);
	}

	/**
	 * A 'streamless' version of encode that simply takes a buffer of bytes and
	 * returns a string containing the encoded buffer.
	 */
	public String encodeBuffer(byte aBuffer[]) {
		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer);
		try {
			encodeBuffer(inStream, outStream);
		} catch (Exception IOException) {
			// This should never happen.
			throw new Error("CharacterEncoder.encodeBuffer internal error");
		}
		return (outStream.toString());
	}

	/**
	 * Encode the <i>aBuffer</i> ByteBuffer and write the encoded result to the
	 * OutputStream <i>aStream</i>.
	 * <P>
	 * The ByteBuffer's position will be advanced to ByteBuffer's limit.
	 */
	public void encodeBuffer(ByteBuffer aBuffer, OutputStream aStream) throws IOException {
		byte[] buf = getBytes(aBuffer);
		encodeBuffer(buf, aStream);
	}

	/**
	 * A 'streamless' version of encode that simply takes a ByteBuffer and
	 * returns a string containing the encoded buffer.
	 * <P>
	 * The ByteBuffer's position will be advanced to ByteBuffer's limit.
	 */
	public String encodeBuffer(ByteBuffer aBuffer) {
		byte[] buf = getBytes(aBuffer);
		return encodeBuffer(buf);
	}

}
